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 <noreply@anthropic.com>
410 lines
21 KiB
C#
410 lines
21 KiB
C#
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;
|
|
|
|
/// <summary>Semantische Statusstufe einer Zeile bzw. eines Hinweises. Bewusst ohne Farbwert im
|
|
/// ViewModel: Die Zuordnung zu einem Pinsel passiert über Style-Klassen in XAML
|
|
/// (<c>Styles/SemanticBrushes.axaml</c>), damit ein Theme-Wechsel hell/dunkel sie live mitnimmt —
|
|
/// ein hart codierter Hex-String aus dem ViewModel bliebe beim Wechsel unverändert stehen.</summary>
|
|
public enum ClassTeacherStatusKind { Ok, Info, Warning, Danger }
|
|
|
|
public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, bool HasAbsenceToday,
|
|
string? AbsenceTooltip, bool HasRecentClassRegisterEntry)
|
|
{
|
|
public ClassAbsenceDaySummaryRow? TodayAbsence { get; init; }
|
|
public bool HasClassRegisterToday { get; init; }
|
|
public bool IsLate => TodayAbsence is { } a &&
|
|
(a.TotalAbsentPeriods == 0 || a.AbsenceReasons.Any(IsLateReason));
|
|
public bool IsUnexcused => TodayAbsence?.Statuses.Any(s =>
|
|
s.Contains("nicht entsch", StringComparison.OrdinalIgnoreCase)) == true;
|
|
public bool NeedsAttention => HasAbsenceToday || HasRecentClassRegisterEntry;
|
|
public int AttentionRank => IsUnexcused ? 0 : IsLate ? 1 : HasAbsenceToday ? 2 :
|
|
HasRecentClassRegisterEntry ? 3 : 4;
|
|
|
|
public string Initials
|
|
{
|
|
get
|
|
{
|
|
var parts = StudentName.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
return string.Concat(parts.Take(2).Select(p => char.ToUpperInvariant(p[0])));
|
|
}
|
|
}
|
|
|
|
public string StatusText
|
|
{
|
|
get
|
|
{
|
|
if (TodayAbsence is null)
|
|
return HasRecentClassRegisterEntry ? "Neuer Klassenbucheintrag" : "Keine gemeldete Fehlzeit";
|
|
var core = IsLate
|
|
? $"{TodayAbsence.TotalAbsentMinutes} Min. verspätet"
|
|
: TodayAbsence.CountsAsFullDay
|
|
? "Ganzer Fehltag"
|
|
: $"{TodayAbsence.TotalAbsentPeriods} Fehlstunde{(TodayAbsence.TotalAbsentPeriods == 1 ? "" : "n")}";
|
|
return IsUnexcused ? $"{core} · Unentschuldigt" : core;
|
|
}
|
|
}
|
|
|
|
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). <see cref="SchoolDaysElapsed"/> 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);
|
|
|
|
public static IReadOnlyList<ClassTeacherRosterRow> Build(
|
|
IReadOnlyList<UntisStudentRosterCacheEntry> students,
|
|
IReadOnlyList<ClassAbsenceDaySummaryRow> todayAbsences,
|
|
IReadOnlyList<UntisForeignClassRegisterEventDto> recentClassRegisterEntries,
|
|
DateOnly? today = null,
|
|
IReadOnlyList<ClassAbsenceDaySummaryRow>? 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)
|
|
.GroupBy(a => a.ExternKey!.Value).ToDictionary(g => g.Key, g => g.First());
|
|
var absenceByName = todayAbsences.GroupBy(a => UntisNameMatching.NameKey(a.StudentName))
|
|
.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 =>
|
|
{
|
|
var nameKey = UntisNameMatching.NameKey(student.DisplayName);
|
|
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();
|
|
}
|
|
|
|
private static bool TryDate(int value, out DateOnly date) =>
|
|
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
|
|
}
|
|
|
|
/// 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 <see cref="ClassTeacherRosterRow.AttentionRank"/>) und ihre Breiten
|
|
/// summieren sich exakt zu <see cref="TotalBarWidth"/>.
|
|
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<ClassTeacherRosterRow> Roster { get; } = [];
|
|
public ObservableCollection<ClassTeacherRosterRow> PrimaryRoster { get; } = [];
|
|
public ObservableCollection<ClassTeacherRosterRow> SecondaryRoster { get; } = [];
|
|
public ObservableCollection<ClassTeacherTrendDay> TrendDays { get; } = [];
|
|
public ObservableCollection<ClassTeacherPatternNotice> PatternNotices { get; } = [];
|
|
|
|
[ObservableProperty] private string? _homeroomClassName;
|
|
[ObservableProperty] private int _activeTabIndex;
|
|
[ObservableProperty] private string _status = "";
|
|
[ObservableProperty] private bool _busy;
|
|
[ObservableProperty] private string _searchText = "";
|
|
[ObservableProperty] private int _selectedRosterFilter;
|
|
[ObservableProperty] private string _primarySectionTitle = "Heute auffällig";
|
|
[ObservableProperty] private string _secondarySectionTitle = "Weitere Schüler*innen";
|
|
[ObservableProperty] private int _studentCount;
|
|
[ObservableProperty] private int _todayAlertCount;
|
|
[ObservableProperty] private int _todayUnexcusedCount;
|
|
[ObservableProperty] private int _recentClassRegisterCount;
|
|
[ObservableProperty] private int _presentCount;
|
|
[ObservableProperty] private int _lateCount;
|
|
[ObservableProperty] private int _excusedAbsenceCount;
|
|
[ObservableProperty] private int _unexcusedAbsenceCount;
|
|
[ObservableProperty] private string _lastUpdatedLabel = "Noch nicht aktualisiert";
|
|
|
|
public bool HomeroomClassConfigured => !string.IsNullOrWhiteSpace(HomeroomClassName);
|
|
public bool HasPrimaryRoster => PrimaryRoster.Count > 0;
|
|
public bool HasSecondaryRoster => SecondaryRoster.Count > 0;
|
|
public bool HasNoFilterResults => !Busy && PrimaryRoster.Count == 0 && SecondaryRoster.Count == 0;
|
|
public bool HasPatternNotices => PatternNotices.Count > 0;
|
|
public bool AlertsFilterSelected => SelectedRosterFilter == 0;
|
|
public bool ClassRegisterFilterSelected => SelectedRosterFilter == 1;
|
|
public bool AllFilterSelected => SelectedRosterFilter == 2;
|
|
public int TodayUnexcusedPercent => Percent(TodayUnexcusedCount);
|
|
public int LatePercent => Percent(LateCount);
|
|
public int PresentPercent => Percent(PresentCount);
|
|
public int ExcusedAbsencePercent => Percent(ExcusedAbsenceCount);
|
|
public int UnexcusedAbsencePercent => Percent(UnexcusedAbsenceCount);
|
|
|
|
public Func<Task>? OnNavigateToSettings { get; set; }
|
|
public Func<Task>? OnNavigateToWorkload { get; set; }
|
|
|
|
public ClassTeacherOverviewViewModel(WebUntisSettingsService settings,
|
|
UntisReportCacheService cache, SchoolYearService schoolYear, ClassTeacherDetailsViewModel detailsTab)
|
|
{
|
|
_settings = settings;
|
|
_cache = cache;
|
|
_schoolYear = schoolYear;
|
|
DetailsTab = detailsTab;
|
|
}
|
|
|
|
partial void OnHomeroomClassNameChanged(string? value) => OnPropertyChanged(nameof(HomeroomClassConfigured));
|
|
partial void OnSearchTextChanged(string value) => ApplyRosterFilter();
|
|
partial void OnSelectedRosterFilterChanged(int value)
|
|
{
|
|
OnPropertyChanged(nameof(AlertsFilterSelected));
|
|
OnPropertyChanged(nameof(ClassRegisterFilterSelected));
|
|
OnPropertyChanged(nameof(AllFilterSelected));
|
|
ApplyRosterFilter();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task Load()
|
|
{
|
|
HomeroomClassName = _settings.HomeroomClassName;
|
|
ActiveTabIndex = 0;
|
|
Roster.Clear(); PrimaryRoster.Clear(); SecondaryRoster.Clear(); TrendDays.Clear(); PatternNotices.Clear();
|
|
if (!HomeroomClassConfigured)
|
|
{
|
|
Status = "Noch keine Klasse ausgewählt.";
|
|
NotifyRosterState();
|
|
return;
|
|
}
|
|
|
|
var className = HomeroomClassName!;
|
|
DetailsTab.Initialize(className);
|
|
Busy = true;
|
|
NotifyRosterState();
|
|
try
|
|
{
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
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, yearStart, today);
|
|
var classRegisterTask = _cache.GetClassRegisterEventsAsync(className, sevenDayStart, today);
|
|
await Task.WhenAll(studentsTask, absencesTask, classRegisterTask);
|
|
|
|
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, absenceDaysYear, schoolDaysElapsed)) Roster.Add(row);
|
|
|
|
StudentCount = Roster.Count;
|
|
TodayAlertCount = Roster.Count(r => r.HasAbsenceToday);
|
|
TodayUnexcusedCount = Roster.Count(r => r.IsUnexcused);
|
|
LateCount = Roster.Count(r => r.IsLate);
|
|
PresentCount = Roster.Count(r => !r.HasAbsenceToday);
|
|
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(absenceDaysYear, trendDays);
|
|
BuildPatternNotices(weekAbsenceDays);
|
|
LastUpdatedLabel = $"Zuletzt aktualisiert: Heute, {DateTime.Now:HH:mm}";
|
|
Status = $"{StudentCount} Schüler*innen · {TodayAlertCount} heute auffällig";
|
|
ApplyRosterFilter();
|
|
NotifySummary();
|
|
}
|
|
catch (WebUntisIntegrationException ex) { Status = ex.Message; NotifyRosterState(); }
|
|
finally { Busy = false; NotifyRosterState(); }
|
|
}
|
|
|
|
[RelayCommand] private void ShowAlerts() => SelectedRosterFilter = 0;
|
|
[RelayCommand] private void ShowClassRegister() => SelectedRosterFilter = 1;
|
|
[RelayCommand] private void ShowAll() => SelectedRosterFilter = 2;
|
|
|
|
[RelayCommand]
|
|
private void OpenClassRegister()
|
|
{
|
|
DetailsTab.StudentFilter = "";
|
|
ActiveTabIndex = 1;
|
|
DetailsTab.LoadCommand.Execute(null);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void OpenAbsences()
|
|
{
|
|
DetailsTab.StudentFilter = "";
|
|
ActiveTabIndex = 2;
|
|
DetailsTab.LoadCommand.Execute(null);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void ShowDetailsForStudent(ClassTeacherRosterRow? row)
|
|
{
|
|
if (row is null) return;
|
|
DetailsTab.StudentFilter = row.StudentName;
|
|
ActiveTabIndex = row.HasAbsenceToday ? 2 : 1;
|
|
DetailsTab.LoadCommand.Execute(null);
|
|
}
|
|
|
|
[RelayCommand] private async Task GoToSettings()
|
|
{ if (OnNavigateToSettings is not null) await OnNavigateToSettings(); }
|
|
|
|
[RelayCommand] private async Task GoToWorkload()
|
|
{ if (OnNavigateToWorkload is not null) await OnNavigateToWorkload(); }
|
|
|
|
private void ApplyRosterFilter()
|
|
{
|
|
PrimaryRoster.Clear(); SecondaryRoster.Clear();
|
|
var query = Roster.Where(r => string.IsNullOrWhiteSpace(SearchText) ||
|
|
r.StudentName.Contains(SearchText.Trim(), StringComparison.CurrentCultureIgnoreCase));
|
|
if (SelectedRosterFilter == 0)
|
|
{
|
|
PrimarySectionTitle = "Heute auffällig";
|
|
foreach (var row in query.Where(r => r.HasAbsenceToday)) PrimaryRoster.Add(row);
|
|
}
|
|
else if (SelectedRosterFilter == 1)
|
|
{
|
|
PrimarySectionTitle = "Klassenbucheinträge der letzten 7 Tage";
|
|
foreach (var row in query.Where(r => r.HasRecentClassRegisterEntry)) PrimaryRoster.Add(row);
|
|
}
|
|
else
|
|
{
|
|
PrimarySectionTitle = "Heute auffällig";
|
|
SecondarySectionTitle = "Weitere Schüler*innen";
|
|
foreach (var row in query.Where(r => r.NeedsAttention)) PrimaryRoster.Add(row);
|
|
foreach (var row in query.Where(r => !r.NeedsAttention)) SecondaryRoster.Add(row);
|
|
}
|
|
NotifyRosterState();
|
|
}
|
|
|
|
/// Letzte <paramref name="days"/> statt letzte 7 Kalendertage (Nutzer-Feedback: an zwei von
|
|
/// sieben Tagen war der Trend bislang systematisch leer, weil Wochenenden mitzählten). Ohne
|
|
/// Ferienkalender — siehe <see cref="LastSchoolDays"/>.
|
|
private void BuildTrend(IReadOnlyList<ClassAbsenceDaySummaryRow> absenceDays, IReadOnlyList<DateOnly> days)
|
|
{
|
|
var today = days[^1];
|
|
var counts = days.Select(date =>
|
|
{
|
|
var rows = absenceDays.Where(r => r.Date == date).ToList();
|
|
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.Total));
|
|
foreach (var day in counts)
|
|
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 <paramref name="count"/> Werktage bis einschließlich <paramref name="end"/>, ohne
|
|
/// Ferienkalender (bewusste Vereinfachung, siehe TODO.md 12.4-Nachtrag) — die App kennt keine
|
|
/// Schulferien, nur Wochenenden.
|
|
private static IReadOnlyList<DateOnly> LastSchoolDays(DateOnly end, int count)
|
|
{
|
|
var days = new List<DateOnly>();
|
|
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 <see cref="ClassTeacherRosterRow.YearAbsenceRatePercent"/> — 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<ClassAbsenceDaySummaryRow> absenceDays)
|
|
{
|
|
foreach (var group in absenceDays.GroupBy(row => UntisNameMatching.NameKey(row.StudentName)))
|
|
{
|
|
var rows = group.ToList();
|
|
var displayName = Roster.FirstOrDefault(row =>
|
|
UntisNameMatching.NameKey(row.StudentName) == group.Key)?.StudentName ?? rows[0].StudentDisplayName;
|
|
var lateDays = rows.Count(row => row.IsLate);
|
|
var unexcusedDays = rows.Count(row => row.IsUnexcused && !row.IsLate);
|
|
if (unexcusedDays >= 2)
|
|
PatternNotices.Add(new ClassTeacherPatternNotice(displayName,
|
|
$"{unexcusedDays} unentschuldigte Fehltage in 7 Tagen", ClassTeacherStatusKind.Danger));
|
|
else if (lateDays >= 2)
|
|
PatternNotices.Add(new ClassTeacherPatternNotice(displayName,
|
|
$"{lateDays}-mal verspätet in 7 Tagen", ClassTeacherStatusKind.Warning));
|
|
}
|
|
OnPropertyChanged(nameof(HasPatternNotices));
|
|
}
|
|
|
|
private int Percent(int value) => StudentCount == 0 ? 0 : (int)Math.Round(100d * value / StudentCount);
|
|
private void NotifySummary()
|
|
{
|
|
OnPropertyChanged(nameof(TodayUnexcusedPercent)); OnPropertyChanged(nameof(LatePercent));
|
|
OnPropertyChanged(nameof(PresentPercent)); OnPropertyChanged(nameof(ExcusedAbsencePercent));
|
|
OnPropertyChanged(nameof(UnexcusedAbsencePercent));
|
|
}
|
|
private void NotifyRosterState()
|
|
{
|
|
OnPropertyChanged(nameof(HasPrimaryRoster)); OnPropertyChanged(nameof(HasSecondaryRoster));
|
|
OnPropertyChanged(nameof(HasNoFilterResults));
|
|
}
|
|
}
|