diff --git a/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs b/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs
index 327a50b..37433d7 100644
--- a/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs
+++ b/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs
@@ -224,11 +224,44 @@ public sealed class ClassTeacherViewModelsTests
}
[Fact]
- public void TrendDay_BalkenbreitenSummierenSichZurGesamtbreite()
+ public void TrendDay_AnteileSummierenSichZurGesamtquote()
{
- var day = new ClassTeacherTrendDay("Mo", 5, 2, 1, 2, 40d, 20d, 40d);
+ var day = new ClassTeacherTrendDay("Mo", 5, 2, 1, 2, 0.4d, 0.2d, 0.4d);
- Assert.Equal(100d, day.TotalBarWidth);
+ Assert.Equal(1.0d, day.TotalFraction, precision: 10);
+ }
+
+ [Fact]
+ public void RosterBuild_StelltGefahrUndWarnungMitSymbolVoran()
+ {
+ var students = new[] { Student(1001, "Ada Müller"), Student(1002, "Ben Schmidt") };
+ var todayAbsences = new[]
+ {
+ new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 26), "Müller Ada", 1001, 2, 90,
+ ["Che"], [1, 2], ["nicht entsch."], ["Absent"], null, null, false),
+ new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 26), "Schmidt Ben", 1002, 2, 90,
+ ["Che"], [1, 2], ["entsch."], ["Absent"], null, null, false),
+ };
+
+ var roster = ClassTeacherRosterRow.Build(students, todayAbsences, [], new DateOnly(2026, 8, 26));
+
+ var ada = Assert.Single(roster, r => r.StudentName == "Ada Müller");
+ Assert.Equal(ClassTeacherStatusKind.Danger, ada.StatusKind);
+ Assert.StartsWith("✕ ", ada.StatusTextWithGlyph);
+
+ var ben = Assert.Single(roster, r => r.StudentName == "Ben Schmidt");
+ Assert.Equal(ClassTeacherStatusKind.Warning, ben.StatusKind);
+ Assert.StartsWith("△ ", ben.StatusTextWithGlyph);
+ }
+
+ [Fact]
+ public void RosterBuild_LaesstUnauffaelligenStatusOhneSymbol()
+ {
+ var students = new[] { Student(1001, "Zora Unauffällig") };
+
+ var row = Assert.Single(ClassTeacherRosterRow.Build(students, [], [], new DateOnly(2026, 8, 26)));
+
+ Assert.Equal(row.StatusText, row.StatusTextWithGlyph);
}
private static UntisStudentRosterCacheEntry Student(int? externKey, string displayName) =>
diff --git a/LehrerApp.Desktop.Tests/FractionWidthConverterTests.cs b/LehrerApp.Desktop.Tests/FractionWidthConverterTests.cs
new file mode 100644
index 0000000..07f201f
--- /dev/null
+++ b/LehrerApp.Desktop.Tests/FractionWidthConverterTests.cs
@@ -0,0 +1,37 @@
+using System.Globalization;
+using LehrerApp.Desktop.Converters;
+using Xunit;
+
+namespace LehrerApp.Desktop.Tests;
+
+public sealed class FractionWidthConverterTests
+{
+ [Fact]
+ public void Convert_MultipliziertTrackbreiteMitAnteil()
+ {
+ var result = FractionWidthConverter.Instance.Convert(
+ [200d, 0.25d], typeof(double), null, CultureInfo.InvariantCulture);
+
+ Assert.Equal(50d, result);
+ }
+
+ [Fact]
+ public void Convert_LiefertNullBreiteBeiUngueltigenWerten()
+ {
+ // Bounds.Width kann vor dem ersten Layout-Pass NaN sein (Element noch nicht gemessen) —
+ // darf nicht zu einer negativen oder undefinierten Breite führen.
+ Assert.Equal(0d, FractionWidthConverter.Instance.Convert(
+ [double.NaN, 0.5d], typeof(double), null, CultureInfo.InvariantCulture));
+ Assert.Equal(0d, FractionWidthConverter.Instance.Convert(
+ ["not a double", 0.5d], typeof(double), null, CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void Convert_ClamptNegativeErgebnisseAufNull()
+ {
+ var result = FractionWidthConverter.Instance.Convert(
+ [100d, -0.2d], typeof(double), null, CultureInfo.InvariantCulture);
+
+ Assert.Equal(0d, result);
+ }
+}
diff --git a/LehrerApp.Desktop/Converters/FractionWidthConverter.cs b/LehrerApp.Desktop/Converters/FractionWidthConverter.cs
new file mode 100644
index 0000000..2380748
--- /dev/null
+++ b/LehrerApp.Desktop/Converters/FractionWidthConverter.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace LehrerApp.Desktop.Converters;
+
+/// Multipliziert die gerenderte Breite eines Referenzelements (typischerweise die neutrale
+/// Track-Leiste eines gestapelten Balkens) mit einem Anteil 0…1. Ersetzt eine im ViewModel hart
+/// codierte Pixelkonstante (Quick-Win, Nutzer-Feedback) — die Segmentbreite bleibt damit auch
+/// korrekt, wenn sich die Breite der Seitenspalte in XAML mal ändert, weil sie sich aus der
+/// tatsächlichen Bounds.Width des Track-Elements ergibt statt aus einer angenommenen
+/// festen Breite. `Grid.ColumnDefinitions` ließ sich dafür nicht binden (Avalonia bietet dort bei
+/// kompilierten Bindings keinen Setter — AVLN3000), deshalb MultiBinding auf Pixelbreite
+/// statt Grid-Sternspalten.
+public sealed class FractionWidthConverter : IMultiValueConverter
+{
+ public static readonly FractionWidthConverter Instance = new();
+
+ public object? Convert(IList