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 @@