using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Interfaces; using LehrerApp.Desktop.Services; namespace LehrerApp.Desktop.ViewModels.Groups; /// Eine Zeile aus dem "-alle-"-Klassenbuchbericht, die eine andere Lehrkraft angelegt hat. /// Rein zur Ansicht - anders als beim Dokumentations-Abgleich (eigene Einträge) gibt es hier kein /// "Übernehmen", da es keine eigenen Einträge zum lokalen Synchronisieren sind. public sealed record ClassTeacherClassRegisterRow(DateOnly Date, string? Subject, string StudentName, string? TeacherUsername, string? CategoryName, string? CategoryGroup, string? Text) { public string DateLabel => Date.ToString("dd.MM.yyyy"); } /// Fehlzeiten eines/einer Schüler*in an einem Tag, über alle Fächer zusammengefasst - /// der WebUntis-Bericht liefert eine Zeile pro Fehlstunde, was bei einer ganzen Klasse über mehrere /// Wochen zu unübersichtlich wäre (Nutzer-Feedback: kompakte "auf einen Blick"-Übersicht statt /// Rohdaten). Die Rohzeilen () bleiben trotzdem erreichbar, /// falls später ein Detail-Drill-down pro Schüler*in dazukommt (siehe TODO.md). public sealed record ClassAbsenceDaySummaryRow(DateOnly Date, string StudentName, int? ExternKey, int TotalAbsentPeriods, int TotalAbsentMinutes, IReadOnlyList Subjects, IReadOnlyList PeriodNumbers, IReadOnlyList Statuses, IReadOnlyList AbsenceReasons, string? Note, string? ExcuseNote, bool CountsAsFullDay) { public string DateLabel => Date.ToString("dd.MM.yyyy"); public string SubjectsLabel => string.Join(", ", Subjects); public string PeriodsLabel => string.Join(", ", PeriodNumbers.Order()); public string StatusLabel => string.Join(", ", Statuses); public string ReasonLabel => string.Join("; ", AbsenceReasons); public static IReadOnlyList GroupByStudentAndDay( IEnumerable entries) => entries .Select(e => (Entry: e, Date: TryDate(e.Date, out var d) ? d : (DateOnly?)null)) .Where(x => x.Date is not null) .GroupBy(x => (Date: x.Date!.Value, x.Entry.StudentName, x.Entry.ExternKey)) .Select(g => new ClassAbsenceDaySummaryRow( g.Key.Date, g.Key.StudentName, g.Key.ExternKey, g.Sum(x => x.Entry.AbsentPeriods), g.Sum(x => x.Entry.AbsentMinutes), Distinct(g.Select(x => x.Entry.Subject)), g.Select(x => x.Entry.PeriodNumber).Where(p => p is not null).Select(p => p!.Value).Distinct().ToList(), Distinct(g.Select(x => x.Entry.Status)), Distinct(g.Select(x => x.Entry.AbsenceReason)), g.Select(x => x.Entry.Note).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)), g.Select(x => x.Entry.ExcuseNote).FirstOrDefault(n => !string.IsNullOrWhiteSpace(n)), g.Any(x => x.Entry.CountsAsFullDay))) .OrderByDescending(r => r.Date).ThenBy(r => r.StudentName) .ToList(); private static List Distinct(IEnumerable values) => values.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v!).Distinct().ToList(); private static bool TryDate(int value, out DateOnly date) => DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date); } /// /// Klassenlehrer-Tab: zeigt Klassenbucheinträge, die andere Lehrkräfte zu Schülern dieser Klasse /// angelegt haben (WebUntis "-alle-"-Bericht, gefiltert auf fremde statt der eigenen /// Benutzerkennung wie beim bestehenden Dokumentations-Abgleich, siehe /// ), sowie die /// Fehlzeiten der Klasse über alle Fächer (). /// Beides rein zur Ansicht, kein Übernehmen in lokale Daten. Nur für Gruppen mit /// sichtbar (siehe /// GroupDetailViewModel/GroupDetailView). /// public partial class ClassTeacherTabViewModel : ObservableObject { private readonly WebUntisIntegrationService _untis; private readonly IGroupRepository _groups; private string _className = ""; public ObservableCollection Entries { get; } = []; public ObservableCollection AbsenceEntries { get; } = []; [ObservableProperty] private DateTimeOffset _startDate = DateTimeOffset.Now.AddDays(-7); [ObservableProperty] private DateTimeOffset _endDate = DateTimeOffset.Now; [ObservableProperty] private string _status = "Zeitraum wählen und laden."; [ObservableProperty] private bool _busy; public ClassTeacherTabViewModel(WebUntisIntegrationService untis, IGroupRepository groups) { _untis = untis; _groups = groups; } public void Initialize(Guid groupId) { _className = _groups.GetById(groupId)?.Name ?? ""; Entries.Clear(); AbsenceEntries.Clear(); Status = "Zeitraum wählen und laden."; } [RelayCommand] private async Task Load() { var start = DateOnly.FromDateTime(StartDate.LocalDateTime); var end = DateOnly.FromDateTime(EndDate.LocalDateTime); if (end < start) { Status = "Das Enddatum darf nicht vor dem Startdatum liegen."; return; } if (string.IsNullOrWhiteSpace(_className)) { Status = "Dieser Gruppe fehlt ein Klassenname."; return; } Busy = true; Entries.Clear(); AbsenceEntries.Clear(); try { var classRegisterTask = _untis.GetForeignClassRegisterEventsAsync(_className, start, end); var absencesTask = _untis.GetClassAbsencesAsync(_className, start, end); await Task.WhenAll(classRegisterTask, absencesTask); var ordered = classRegisterTask.Result .Select(e => (Entry: e, Date: TryDate(e.Date, out var d) ? d : (DateOnly?)null)) .Where(x => x.Date is not null) .OrderByDescending(x => x.Date).ThenBy(x => x.Entry.StudentName); foreach (var (entry, date) in ordered) { Entries.Add(new ClassTeacherClassRegisterRow(date!.Value, entry.Subject, entry.StudentName, entry.TeacherUsername, entry.CategoryName, entry.CategoryGroup, entry.Text)); } foreach (var row in ClassAbsenceDaySummaryRow.GroupByStudentAndDay(absencesTask.Result)) AbsenceEntries.Add(row); Status = $"{Entries.Count} Klassenbucheinträge anderer Lehrkräfte, " + $"{AbsenceEntries.Count} Fehlzeiten-Tage im Zeitraum."; } catch (WebUntisIntegrationException ex) { Status = ex.Message; } finally { Busy = false; } } private static bool TryDate(int value, out DateOnly date) => DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date); }