From 9d375caf14dfa732ddfcd260789c66d2a43331b1 Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Thu, 27 Aug 2026 12:47:13 +0200 Subject: [PATCH] Klassenlehrer-Tab: Theme-Ressourcen statt Hex-Farben, gestapelter Trend, Fehlquote seit Schuljahresbeginn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Semantische Brushes (Light/Dark) lösen die fest codierten Dark-Hex-Werte ab, die im hellen Theme unbenutzbar waren. Trend-Chart zeigt jetzt einen echten gestapelten Balken über die letzten 7 Schultage statt drei unabhängig skalierten über Kalendertage; neue Fehlquote je Schüler*in seit Schuljahresbeginn ergänzt den Heute-Snapshot. Co-Authored-By: Claude Sonnet 5 --- .../ClassTeacherViewModelsTests.cs | 62 +++++- LehrerApp.Desktop/App.axaml | 8 + .../Styles/SemanticBrushes.axaml | 63 ++++++ .../ClassTeacherDetailsViewModel.cs | 16 +- .../ClassTeacherOverviewViewModel.cs | 148 ++++++++++--- .../ClassTeacherAbsencesView.axaml | 43 +++- .../ClassTeacherOverviewView.axaml | 208 ++++++++++++------ .../ClassTeacherRegisterView.axaml | 32 ++- TODO.md | 49 +++++ 9 files changed, 510 insertions(+), 119 deletions(-) create mode 100644 LehrerApp.Desktop/Styles/SemanticBrushes.axaml diff --git a/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs b/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs index c20d5e4..327a50b 100644 --- a/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs +++ b/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs @@ -142,7 +142,7 @@ public sealed class ClassTeacherViewModelsTests Assert.True(row.IsLate); Assert.True(row.IsUnexcused); Assert.Equal("20 Min. verspätet · Unentschuldigt", row.StatusText); - Assert.Equal("#FF5A67", row.StatusColor); + Assert.Equal(ClassTeacherStatusKind.Danger, row.StatusKind); } [Fact] @@ -171,6 +171,66 @@ public sealed class ClassTeacherViewModelsTests rows.Select(r => r.StudentName)); } + [Fact] + public void RosterBuild_BerechnetFehlquoteSeitSchuljahresbeginn() + { + var students = new[] { Student(1001, "Ada Müller") }; + var yearAbsences = new[] + { + new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 10), "Müller Ada", 1001, 2, 90, + ["Che"], [1, 2], ["entsch."], ["Krank"], null, null, false), + new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 17), "Müller Ada", 1001, 6, 270, + ["Che"], [1, 2, 3, 4, 5, 6], ["nicht entsch."], ["Absent"], null, null, true), + }; + + var row = Assert.Single(ClassTeacherRosterRow.Build(students, [], [], + new DateOnly(2026, 8, 26), yearAbsences, schoolDaysElapsed: 20)); + + Assert.True(row.HasYearSummary); + Assert.Equal(2, row.YearAbsenceDayCount); + Assert.Equal(1, row.YearUnexcusedDayCount); + Assert.Equal(10, row.YearAbsenceRatePercent); + Assert.Contains("10 %", row.YearSummaryLabel); + Assert.Contains("2 von 20", row.YearSummaryTooltip); + Assert.Contains("1 unentschuldigt", row.YearSummaryTooltip); + } + + [Fact] + public void RosterBuild_OhneJahresdatenZeigtKeineFehlquote() + { + var students = new[] { Student(1001, "Ada Müller") }; + + var row = Assert.Single(ClassTeacherRosterRow.Build(students, [], [], new DateOnly(2026, 8, 26))); + + Assert.False(row.HasYearSummary); + Assert.Equal("", row.YearSummaryLabel); + Assert.Null(row.YearSummaryTooltip); + } + + [Fact] + public void RosterBuild_OrdnetJahresfehlzeitenAuchOhneExternKeyUeberNamenZu() + { + var students = new[] { Student(externKey: null, displayName: "Ben Schmidt") }; + var yearAbsences = new[] + { + new ClassAbsenceDaySummaryRow(new DateOnly(2026, 8, 10), "Schmidt Ben", null, 2, 90, + ["Che"], [1, 2], ["entsch."], ["Krank"], null, null, false), + }; + + var row = Assert.Single(ClassTeacherRosterRow.Build(students, [], [], new DateOnly(2026, 8, 26), + yearAbsences, schoolDaysElapsed: 10)); + + Assert.Equal(1, row.YearAbsenceDayCount); + } + + [Fact] + public void TrendDay_BalkenbreitenSummierenSichZurGesamtbreite() + { + var day = new ClassTeacherTrendDay("Mo", 5, 2, 1, 2, 40d, 20d, 40d); + + Assert.Equal(100d, day.TotalBarWidth); + } + private static UntisStudentRosterCacheEntry Student(int? externKey, string displayName) => new() { ClassName = "6a", ExternKey = externKey, DisplayName = displayName }; } diff --git a/LehrerApp.Desktop/App.axaml b/LehrerApp.Desktop/App.axaml index 1ff4bad..42534fa 100644 --- a/LehrerApp.Desktop/App.axaml +++ b/LehrerApp.Desktop/App.axaml @@ -2,6 +2,14 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="LehrerApp.Desktop.App" RequestedThemeVariant="Default"> + + + + + + + + diff --git a/LehrerApp.Desktop/Styles/SemanticBrushes.axaml b/LehrerApp.Desktop/Styles/SemanticBrushes.axaml new file mode 100644 index 0000000..93f7451 --- /dev/null +++ b/LehrerApp.Desktop/Styles/SemanticBrushes.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs index 31df481..25c9c73 100644 --- a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs @@ -13,8 +13,11 @@ public sealed record ClassTeacherClassRegisterRow(DateOnly Date, string? Subject { public string DateLabel => Date.ToString("dd.MM.yyyy"); public string StudentDisplayName => DisplayName(StudentName); - public string CategoryColor => CategoryGroup?.Contains("Negativ", StringComparison.OrdinalIgnoreCase) == true - ? "#FF5A67" : "#4C8DFF"; + public ClassTeacherStatusKind CategoryKind => + CategoryGroup?.Contains("Negativ", StringComparison.OrdinalIgnoreCase) == true + ? ClassTeacherStatusKind.Danger : ClassTeacherStatusKind.Info; + public bool IsDangerStatus => CategoryKind == ClassTeacherStatusKind.Danger; + public bool IsInfoStatus => CategoryKind == ClassTeacherStatusKind.Info; private static string DisplayName(string value) { @@ -46,10 +49,6 @@ public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName public string PeriodsLabel => string.Join(", ", PeriodNumbers.Order()); public string StatusLabel => string.Join(", ", Statuses); public string ReasonLabel => string.Join("; ", AbsenceReasons); - /// Nutzer-Feedback: ein ganzer Fehltag ist in der Liste sonst nicht auf einen Blick erkennbar - - /// dezente Hervorhebung statt einer weiteren Text-/Icon-Spalte. - public string RowBackground => CountsAsFullDay ? "#1FFFA000" : "Transparent"; - public string? FullDayTooltip => CountsAsFullDay ? "Ganzer Fehltag" : null; public bool IsLate => TotalAbsentPeriods == 0 || AbsenceReasons.Any(r => r.Contains("verspät", StringComparison.OrdinalIgnoreCase)); public bool IsUnexcused => Statuses.Any(s => @@ -58,7 +57,10 @@ public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName CountsAsFullDay ? "Ganzer Fehltag" : $"{TotalAbsentPeriods} Fehlstunde{(TotalAbsentPeriods == 1 ? "" : "n")}"; public string FriendlyStatusLabel => IsUnexcused ? "Unentschuldigt" : Statuses.Any(s => s.Contains("entsch", StringComparison.OrdinalIgnoreCase)) ? "Entschuldigt" : StatusLabel; - public string StatusColor => IsUnexcused ? "#FF5A67" : IsLate ? "#F59E0B" : "#73C45A"; + public ClassTeacherStatusKind StatusKind => IsUnexcused ? ClassTeacherStatusKind.Danger : + IsLate ? ClassTeacherStatusKind.Warning : ClassTeacherStatusKind.Ok; + public bool IsDangerStatus => StatusKind == ClassTeacherStatusKind.Danger; + public bool IsWarningStatus => StatusKind == ClassTeacherStatusKind.Warning; public string DetailLabel => string.Join(" · ", new[] { ReasonLabel, Note, ExcuseNote } .Where(value => !string.IsNullOrWhiteSpace(value))); diff --git a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs index 6873416..71b2e7d 100644 --- a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs @@ -2,10 +2,17 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Models; +using LehrerApp.Core.Services; using LehrerApp.Desktop.Services; namespace LehrerApp.Desktop.ViewModels.ClassTeacher; +/// Semantische Statusstufe einer Zeile bzw. eines Hinweises. Bewusst ohne Farbwert im +/// ViewModel: Die Zuordnung zu einem Pinsel passiert über Style-Klassen in XAML +/// (Styles/SemanticBrushes.axaml), damit ein Theme-Wechsel hell/dunkel sie live mitnimmt — +/// ein hart codierter Hex-String aus dem ViewModel bliebe beim Wechsel unverändert stehen. +public enum ClassTeacherStatusKind { Ok, Info, Warning, Danger } + public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, bool HasAbsenceToday, string? AbsenceTooltip, bool HasRecentClassRegisterEntry) { @@ -43,12 +50,30 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b } } - public string StatusColor => IsUnexcused ? "#FF5A67" : - HasAbsenceToday ? "#F59E0B" : HasRecentClassRegisterEntry ? "#4C8DFF" : "#73C45A"; - public string StatusBarColor => StatusColor; + public ClassTeacherStatusKind StatusKind => IsUnexcused ? ClassTeacherStatusKind.Danger : + HasAbsenceToday ? ClassTeacherStatusKind.Warning : + HasRecentClassRegisterEntry ? ClassTeacherStatusKind.Info : ClassTeacherStatusKind.Ok; + public bool IsDangerStatus => StatusKind == ClassTeacherStatusKind.Danger; + public bool IsWarningStatus => StatusKind == ClassTeacherStatusKind.Warning; + public bool IsInfoStatus => StatusKind == ClassTeacherStatusKind.Info; public string ClassRegisterLabel => HasClassRegisterToday ? "Eintrag heute" : HasRecentClassRegisterEntry ? "Eintrag diese Woche" : ""; + /// Kumulierte Fehlzeiten seit Schuljahresbeginn (Nutzer-Feedback: der Heute-Snapshot allein + /// sagt für Zeugnis/Attestpflicht wenig aus). zählt nur + /// Werktage, ohne Ferienkalender — eine bewusste Vereinfachung, siehe TODO.md 12.4-Nachtrag. + public int YearAbsenceDayCount { get; init; } + public int YearUnexcusedDayCount { get; init; } + public int SchoolDaysElapsed { get; init; } + public bool HasYearSummary => SchoolDaysElapsed > 0 && YearAbsenceDayCount > 0; + public int YearAbsenceRatePercent => + SchoolDaysElapsed <= 0 ? 0 : (int)Math.Round(100d * YearAbsenceDayCount / SchoolDaysElapsed); + public string YearSummaryLabel => HasYearSummary + ? $"{YearAbsenceRatePercent} % Fehlzeit seit Schuljahresbeginn" : ""; + public string? YearSummaryTooltip => !HasYearSummary ? null : + $"{YearAbsenceDayCount} von {SchoolDaysElapsed} Schultagen mit Fehlzeit" + + (YearUnexcusedDayCount > 0 ? $" · {YearUnexcusedDayCount} unentschuldigt" : ""); + private static bool IsLateReason(string reason) => reason.Contains("verspät", StringComparison.OrdinalIgnoreCase); @@ -56,7 +81,9 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b IReadOnlyList students, IReadOnlyList todayAbsences, IReadOnlyList recentClassRegisterEntries, - DateOnly? today = null) + DateOnly? today = null, + IReadOnlyList? yearAbsences = null, + int schoolDaysElapsed = 0) { var referenceDate = today ?? todayAbsences.FirstOrDefault()?.Date ?? DateOnly.FromDateTime(DateTime.Today); var absenceByKey = todayAbsences.Where(a => a.ExternKey is not null) @@ -65,6 +92,11 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b .ToDictionary(g => g.Key, g => g.First()); var recentByName = recentClassRegisterEntries.GroupBy(e => UntisNameMatching.NameKey(e.StudentName)) .ToDictionary(g => g.Key, g => g.ToList()); + var yearRows = yearAbsences ?? []; + var yearByKey = yearRows.Where(a => a.ExternKey is not null) + .GroupBy(a => a.ExternKey!.Value).ToDictionary(g => g.Key, g => g.ToList()); + var yearByName = yearRows.GroupBy(a => UntisNameMatching.NameKey(a.StudentName)) + .ToDictionary(g => g.Key, g => g.ToList()); return students.Select(student => { @@ -72,12 +104,17 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b var absence = (student.ExternKey is { } key ? absenceByKey.GetValueOrDefault(key) : null) ?? absenceByName.GetValueOrDefault(nameKey); var registerEntries = recentByName.GetValueOrDefault(nameKey) ?? []; + var yearEntries = (student.ExternKey is { } yearKey ? yearByKey.GetValueOrDefault(yearKey) : null) + ?? yearByName.GetValueOrDefault(nameKey) ?? []; return new ClassTeacherRosterRow(student.DisplayName, student.ExternKey, absence is not null, absence is null ? null : $"{absence.TotalAbsentPeriods} Stunde(n) — {absence.StatusLabel}", registerEntries.Count > 0) { TodayAbsence = absence, HasClassRegisterToday = registerEntries.Any(e => TryDate(e.Date, out var date) && date == referenceDate), + YearAbsenceDayCount = yearEntries.Count, + YearUnexcusedDayCount = yearEntries.Count(r => r.IsUnexcused), + SchoolDaysElapsed = schoolDaysElapsed, }; }) .OrderBy(r => r.AttentionRank).ThenBy(r => r.StudentName).ToList(); @@ -87,14 +124,31 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date); } -public sealed record ClassTeacherTrendDay(string DayLabel, int AlertCount, int UnexcusedCount, int LateCount, - double AlertBarWidth, double UnexcusedBarWidth, double LateBarWidth); -public sealed record ClassTeacherPatternNotice(string StudentName, string Message, string Color); +/// Ein gestapelter Tages-Balken statt drei unabhängig skalierter (Nutzer-Feedback: die vorherige +/// Version zeigte Alerts/Unentschuldigt/Verspätet als drei separate, gleich hohe Balken, obwohl +/// Unentschuldigt und Verspätet Teilmengen von Alerts sind — das suggerierte fälschlich drei +/// unabhängige Größen). Die drei Segmente sind überschneidungsfrei (Unentschuldigt hat Vorrang +/// vor Verspätet, wie bei ) und ihre Breiten +/// summieren sich exakt zu . +public sealed record ClassTeacherTrendDay(string DayLabel, int AlertCount, int UnexcusedCount, + int LateExcusedCount, int ExcusedCount, double UnexcusedBarWidth, double LateExcusedBarWidth, + double ExcusedBarWidth) +{ + public double TotalBarWidth => UnexcusedBarWidth + LateExcusedBarWidth + ExcusedBarWidth; +} +public sealed record ClassTeacherPatternNotice(string StudentName, string Message, + ClassTeacherStatusKind Kind) +{ + public bool IsDangerStatus => Kind == ClassTeacherStatusKind.Danger; + public bool IsWarningStatus => Kind == ClassTeacherStatusKind.Warning; + public bool IsInfoStatus => Kind == ClassTeacherStatusKind.Info; +} public partial class ClassTeacherOverviewViewModel : ObservableObject { private readonly WebUntisSettingsService _settings; private readonly UntisReportCacheService _cache; + private readonly SchoolYearService _schoolYear; public ClassTeacherDetailsViewModel DetailsTab { get; } public ObservableCollection Roster { get; } = []; @@ -139,10 +193,11 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject public Func? OnNavigateToWorkload { get; set; } public ClassTeacherOverviewViewModel(WebUntisSettingsService settings, - UntisReportCacheService cache, ClassTeacherDetailsViewModel detailsTab) + UntisReportCacheService cache, SchoolYearService schoolYear, ClassTeacherDetailsViewModel detailsTab) { _settings = settings; _cache = cache; + _schoolYear = schoolYear; DetailsTab = detailsTab; } @@ -176,16 +231,24 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject try { var today = DateOnly.FromDateTime(DateTime.Today); - var start = today.AddDays(-6); + var sevenDayStart = today.AddDays(-6); + var trendDays = LastSchoolDays(today, 7); + // Fehlzeiten seit Schuljahresbeginn statt nur der letzten 7 Tage: liefert die + // Kennzahl unten (YearSummaryLabel) und den Nenner für die Fehlquote in einem Abruf. + // UntisReportCacheService cached diesen "kalten" Bereich dauerhaft (siehe dort) — + // teuer ist nur der erste Abruf pro Schuljahr, nicht jedes Öffnen der Ansicht. + var yearStart = _schoolYear.SchoolYearStart(_schoolYear.CurrentSchoolYear(today)); var studentsTask = _cache.GetStudentRosterAsync(className); - var absencesTask = _cache.GetAbsencesAsync(className, start, today); - var classRegisterTask = _cache.GetClassRegisterEventsAsync(className, start, today); + var absencesTask = _cache.GetAbsencesAsync(className, yearStart, today); + var classRegisterTask = _cache.GetClassRegisterEventsAsync(className, sevenDayStart, today); await Task.WhenAll(studentsTask, absencesTask, classRegisterTask); - var absenceDays = ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result); - var todayAbsences = absenceDays.Where(a => a.Date == today).ToList(); + var absenceDaysYear = ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result); + var todayAbsences = absenceDaysYear.Where(a => a.Date == today).ToList(); + var weekAbsenceDays = absenceDaysYear.Where(a => a.Date >= sevenDayStart).ToList(); + var schoolDaysElapsed = CountWeekdays(yearStart, today); foreach (var row in ClassTeacherRosterRow.Build(studentsTask.Result, todayAbsences, - classRegisterTask.Result, today)) Roster.Add(row); + classRegisterTask.Result, today, absenceDaysYear, schoolDaysElapsed)) Roster.Add(row); StudentCount = Roster.Count; TodayAlertCount = Roster.Count(r => r.HasAbsenceToday); @@ -195,8 +258,8 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject ExcusedAbsenceCount = Roster.Count(r => r.HasAbsenceToday && !r.IsLate && !r.IsUnexcused); UnexcusedAbsenceCount = Roster.Count(r => r.HasAbsenceToday && !r.IsLate && r.IsUnexcused); RecentClassRegisterCount = classRegisterTask.Result.Count; - BuildTrend(absenceDays, start, today); - BuildPatternNotices(absenceDays); + BuildTrend(absenceDaysYear, trendDays); + BuildPatternNotices(weekAbsenceDays); LastUpdatedLabel = $"Zuletzt aktualisiert: Heute, {DateTime.Now:HH:mm}"; Status = $"{StudentCount} Schüler*innen · {TodayAlertCount} heute auffällig"; ApplyRosterFilter(); @@ -266,21 +329,50 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject NotifyRosterState(); } - private void BuildTrend(IReadOnlyList absenceDays, DateOnly start, DateOnly end) + /// Letzte statt letzte 7 Kalendertage (Nutzer-Feedback: an zwei von + /// sieben Tagen war der Trend bislang systematisch leer, weil Wochenenden mitzählten). Ohne + /// Ferienkalender — siehe . + private void BuildTrend(IReadOnlyList absenceDays, IReadOnlyList days) { - var counts = Enumerable.Range(0, 7).Select(offset => start.AddDays(offset)).Select(date => + var today = days[^1]; + var counts = days.Select(date => { var rows = absenceDays.Where(r => r.Date == date).ToList(); - return (Date: date, Alerts: rows.Count, - Unexcused: rows.Count(r => r.Statuses.Any(s => s.Contains("nicht entsch", StringComparison.OrdinalIgnoreCase))), - Late: rows.Count(r => r.TotalAbsentPeriods == 0 || r.AbsenceReasons.Any(a => - a.Contains("verspät", StringComparison.OrdinalIgnoreCase)))); + var unexcused = rows.Count(r => r.IsUnexcused); + var lateExcused = rows.Count(r => !r.IsUnexcused && r.IsLate); + var excused = rows.Count(r => !r.IsUnexcused && !r.IsLate); + return (Date: date, Unexcused: unexcused, LateExcused: lateExcused, Excused: excused, + Total: unexcused + lateExcused + excused); }).ToList(); - var max = Math.Max(1, counts.Max(c => c.Alerts)); + var max = Math.Max(1, counts.Max(c => c.Total)); foreach (var day in counts) - TrendDays.Add(new ClassTeacherTrendDay(day.Date == end ? "Heute" : day.Date.ToString("ddd"), - day.Alerts, day.Unexcused, day.Late, - 112d * day.Alerts / max, 112d * day.Unexcused / max, 112d * day.Late / max)); + TrendDays.Add(new ClassTeacherTrendDay(day.Date == today ? "Heute" : day.Date.ToString("ddd"), + day.Total, day.Unexcused, day.LateExcused, day.Excused, + 112d * day.Unexcused / max, 112d * day.LateExcused / max, 112d * day.Excused / max)); + } + + /// Letzte Werktage bis einschließlich , ohne + /// Ferienkalender (bewusste Vereinfachung, siehe TODO.md 12.4-Nachtrag) — die App kennt keine + /// Schulferien, nur Wochenenden. + private static IReadOnlyList LastSchoolDays(DateOnly end, int count) + { + var days = new List(); + for (var cursor = end; days.Count < count; cursor = cursor.AddDays(-1)) + if (cursor.DayOfWeek is not (DayOfWeek.Saturday or DayOfWeek.Sunday)) + days.Add(cursor); + days.Reverse(); + return days; + } + + /// Nenner für — Werktage zwischen + /// Schuljahresbeginn und heute, ebenfalls ohne Ferienkalender. + private static int CountWeekdays(DateOnly start, DateOnly end) + { + if (end < start) return 0; + var count = 0; + for (var d = start; d <= end; d = d.AddDays(1)) + if (d.DayOfWeek is not (DayOfWeek.Saturday or DayOfWeek.Sunday)) count++; + return count; } private void BuildPatternNotices(IReadOnlyList absenceDays) @@ -294,10 +386,10 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject var unexcusedDays = rows.Count(row => row.IsUnexcused && !row.IsLate); if (unexcusedDays >= 2) PatternNotices.Add(new ClassTeacherPatternNotice(displayName, - $"{unexcusedDays} unentschuldigte Fehltage in 7 Tagen", "#FF5A67")); + $"{unexcusedDays} unentschuldigte Fehltage in 7 Tagen", ClassTeacherStatusKind.Danger)); else if (lateDays >= 2) PatternNotices.Add(new ClassTeacherPatternNotice(displayName, - $"{lateDays}-mal verspätet in 7 Tagen", "#F59E0B")); + $"{lateDays}-mal verspätet in 7 Tagen", ClassTeacherStatusKind.Warning)); } OnPropertyChanged(nameof(HasPatternNotices)); } diff --git a/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherAbsencesView.axaml b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherAbsencesView.axaml index 51d2ac0..4550f20 100644 --- a/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherAbsencesView.axaml +++ b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherAbsencesView.axaml @@ -3,6 +3,31 @@ xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher" x:Class="LehrerApp.Desktop.Views.ClassTeacher.ClassTeacherAbsencesView" x:DataType="vm:ClassTeacherDetailsViewModel"> + + + + + + + + + + @@ -10,11 +35,10 @@ + + + - - + @@ -80,32 +154,37 @@ - - - + - - - - - - - + + @@ -189,10 +227,10 @@ - - - - + + + + @@ -205,15 +243,38 @@ - - + + + + + + + + + - + + + + + + + + + + + + + + @@ -224,7 +285,10 @@ - + diff --git a/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherRegisterView.axaml b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherRegisterView.axaml index ee45f27..b9a249d 100644 --- a/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherRegisterView.axaml +++ b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherRegisterView.axaml @@ -3,6 +3,31 @@ xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.ClassTeacher" x:Class="LehrerApp.Desktop.Views.ClassTeacher.ClassTeacherRegisterView" x:DataType="vm:ClassTeacherDetailsViewModel"> + + + + + + + + + + @@ -10,11 +35,10 @@