diff --git a/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs index 79cbc59..5e8fcf9 100644 --- a/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs @@ -28,6 +28,57 @@ public sealed class DashboardViewModelTests Assert.True(vm.CalendarCard.EffectiveIsVisible); } + /// Regression: ApplyCardLayout zaehlte frueher IsVisible statt EffectiveIsVisible. Eine + /// eingeschaltete, aber leere HideWhenEmpty-Kachel belegte damit einen Rasterplatz, den das + /// Grid nie fuellt — im Alltag der Normalfall, weil meist mehrere Hinweiskacheln leer sind. + [Fact] + public void Kachelraster_LeereAusgeblendeteKacheln_HinterlassenKeineLuecke() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + + var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }); + + // Ohne mindestens eine leer ausgeblendete Kachel wuerde der Test nichts pruefen. + Assert.False(vm.ExcusesCard.EffectiveIsVisible); + + var belegtePlaetze = vm.DashboardCards.Where(c => c.EffectiveIsVisible) + .Select(c => c.Row * 2 + c.Column).OrderBy(slot => slot).ToList(); + Assert.Equal(Enumerable.Range(0, belegtePlaetze.Count), belegtePlaetze); + } + + [Fact] + public void KachelMargin_FolgtDerBerechnetenSpalte() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + + var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }); + + foreach (var card in vm.DashboardCards.Where(c => c.EffectiveIsVisible)) + Assert.Equal(card.Column == 0 + ? new Avalonia.Thickness(0, 0, 8, 8) + : new Avalonia.Thickness(8, 0, 0, 8), card.Margin); + } + + /// Regression: der Margin hing fest im XAML an der Kachel. Wandert sie durch Aus-/Einblenden + /// einer vorherigen Kachel in die andere Spalte, sass der Rinnstein auf der falschen Seite. + [Fact] + public void KachelAusblenden_DrehtDenMarginDerNachfolgendenKachel() + { + var group = new LearningGroup { Name = "9c" }; + var today = DateOnly.FromDateTime(DateTime.Today); + + var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }); + Assert.Equal(1, vm.TasksCard.Column); + Assert.Equal(new Avalonia.Thickness(8, 0, 0, 8), vm.TasksCard.Margin); + + vm.TodayCard.IsVisible = false; + + Assert.Equal(0, vm.TasksCard.Column); + Assert.Equal(new Avalonia.Thickness(0, 0, 8, 8), vm.TasksCard.Margin); + } + private static PeriodScheduleService NewPeriodSchedule() { var tempPath = System.IO.Path.Combine( diff --git a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs index 4e8ec57..70ddf85 100644 --- a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs @@ -284,6 +284,10 @@ public partial class DashboardViewModel : ObservableObject SupportCard.IsEmpty = SupportPlanReviews.Count == 0; GroupsCard.IsEmpty = CurrentGroups.Count == 0; + // Erst nachdem alle IsEmpty-Werte stehen: welche Kachel tatsaechlich gerendert wird, haengt + // ueber EffectiveIsVisible daran, und davon wiederum die Zeilen-/Spaltenzuordnung. + ApplyCardLayout(); + OnPropertyChanged(nameof(TodayLessonCount)); OnPropertyChanged(nameof(OpenTaskCount)); OnPropertyChanged(nameof(UpcomingCount)); @@ -673,12 +677,15 @@ public partial class DashboardViewModel : ObservableObject "missingteachingtime" => "Unterrichtszeit nacherfassen", _ => key, }; + // Zaehlt bewusst EffectiveIsVisible, nicht IsVisible: eine eingeschaltete, aber gerade leere + // HideWhenEmpty-Kachel wird nicht gerendert und darf deshalb auch keinen Rasterplatz belegen, + // sonst bleibt an ihrer Stelle eine Luecke im zweispaltigen Grid. private void ApplyCardLayout() { var visibleIndex = 0; foreach (var card in DashboardCards) { - var index = card.IsVisible ? visibleIndex++ : 0; + var index = card.EffectiveIsVisible ? visibleIndex++ : 0; card.Row = index / 2; card.Column = index % 2; } @@ -1262,6 +1269,14 @@ public partial class DashboardCardOption : ObservableObject public bool EffectiveIsVisible => IsVisible && (!HideWhenEmpty || !IsEmpty); public Action? OnVisibilityChanged { get; set; } + /// Rinnstein zur jeweils anderen Rasterspalte. Muss aus der berechneten + /// kommen und darf nicht im XAML fest an der Kachel haengen: welche Kachel + /// links und welche rechts landet, entscheidet sich erst zur Laufzeit aus Reihenfolge und + /// Sichtbarkeit, ein fester Margin sitzt dann bei jeder Umschaltung auf der falschen Seite. + public Avalonia.Thickness Margin => Column == 0 + ? new Avalonia.Thickness(0, 0, 8, 8) + : new Avalonia.Thickness(8, 0, 0, 8); + public DashboardCardOption(string key, string title, bool isVisible) { Key = key; @@ -1278,4 +1293,6 @@ public partial class DashboardCardOption : ObservableObject } partial void OnIsEmptyChanged(bool value) => OnPropertyChanged(nameof(EffectiveIsVisible)); + + partial void OnColumnChanged(int value) => OnPropertyChanged(nameof(Margin)); } diff --git a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml index 2eb49df..a62b98e 100644 --- a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml +++ b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml @@ -146,7 +146,7 @@ @@ -187,7 +187,7 @@ @@ -224,7 +224,7 @@ @@ -368,7 +368,7 @@ @@ -402,7 +402,7 @@ @@ -432,7 +432,7 @@ @@ -456,7 +456,7 @@ @@ -482,7 +482,7 @@ @@ -513,7 +513,7 @@ @@ -550,7 +550,7 @@ @@ -583,7 +583,7 @@ @@ -613,7 +613,7 @@ @@ -647,7 +647,7 @@