using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels.Students; namespace LehrerApp.Desktop.ViewModels.ClassTeacher; /// /// "Vorgänge"-Tab der Klassenlehreransicht: Fallmappen für konkrete, laufende Probleme mit einer/ /// einem oder mehreren Schüler*innen der Klasse, die Dokumentation und (eingefrorene) WebUntis- /// Klassenbucheinträge bündeln. Löst wie die Untis- /// Roster-Namen der Klasse auf lokale -Datensätze auf (bewusst ein eigener, /// nicht geteilter Abgleich — dasselbe kleine Muster steckt schon zweimal im Code, siehe /// , eine gemeinsame Abstraktion dafür ist /// hier nicht Teil der Aufgabe). /// public partial class ClassTeacherCasesViewModel : ObservableObject { private readonly IVorgangRepository _vorgaenge; private readonly IDocumentationRepository _documentation; private readonly IStudentRepository _students; private readonly UntisReportCacheService _cache; private string _className = ""; private List _rosterStudentOptions = []; public ObservableCollection Cases { get; } = []; [ObservableProperty] private bool _onlyOpen = true; [ObservableProperty] private bool _busy; [ObservableProperty] private string _status = ""; public bool HasCases => Cases.Count > 0; public Func, Vorgang?, Task>? OnEditVorgang { get; set; } public Func>? OnConfirmDeleteVorgang { get; set; } public Func, Documentation?, Task>? OnEditDocumentation { get; set; } public ClassTeacherCasesViewModel(IVorgangRepository vorgaenge, IDocumentationRepository documentation, IStudentRepository students, UntisReportCacheService cache) { _vorgaenge = vorgaenge; _documentation = documentation; _students = students; _cache = cache; } public void Initialize(string className) { _className = className; Cases.Clear(); Status = "Wird geladen…"; NotifyState(); _ = LoadInternal(); } partial void OnOnlyOpenChanged(bool value) => _ = LoadInternal(); [RelayCommand] private Task Refresh() => LoadInternal(); private async Task LoadInternal() { if (string.IsNullOrWhiteSpace(_className)) return; // IsRevealed übersteht den Neuaufbau der Liste nicht automatisch (neue VorgangItem-Instanzen // pro Load) - deshalb hier gemerkt und danach wiederhergestellt, sonst klappt eine gerade // aufgeklappte Karte bei jeder Verknüpfungs-/Status-Aktion (die intern neu lädt) wieder zu. var revealedIds = Cases.Where(c => c.IsRevealed).Select(c => c.Model.Id).ToHashSet(); Busy = true; Cases.Clear(); NotifyState(); try { var roster = await _cache.GetStudentRosterAsync(_className); var localStudents = _students.GetAll(); var matches = roster .Select(r => (Roster: r, Student: ClassTeacherOverviewViewModel.MatchStudent(r.DisplayName, localStudents))) .Where(x => x.Student is not null) .Select(x => (StudentId: x.Student!.Id, DisplayName: x.Roster.DisplayName)) .DistinctBy(x => x.StudentId) .ToList(); _rosterStudentOptions = matches.Select(m => new StudentOption(m.StudentId, m.DisplayName)).ToList(); var rosterIds = matches.Select(m => m.StudentId).ToHashSet(); var nameById = matches.ToDictionary(m => m.StudentId, m => m.DisplayName); var relevant = _vorgaenge.GetAll().Where(v => v.StudentIds.Any(rosterIds.Contains)); if (OnlyOpen) relevant = relevant.Where(v => v.Status == VorgangStatus.Open); var allDocs = _documentation.GetAll(); foreach (var vorgang in relevant.OrderByDescending(v => v.UpdatedAt)) { var names = vorgang.StudentIds.Select(id => nameById.GetValueOrDefault(id, "")) .Where(n => n != ""); var item = new VorgangItem(vorgang, string.Join(", ", names), LinkDocumentation, UnlinkDocumentation, RemoveClassRegisterEntry) { IsRevealed = revealedIds.Contains(vorgang.Id) }; var linkedIds = vorgang.DocumentationIds.ToHashSet(); foreach (var doc in allDocs.Where(d => linkedIds.Contains(d.Id))) item.LinkedDocumentation.Add(new DocumentationItem(doc, nameById.GetValueOrDefault(doc.StudentId, ""))); foreach (var doc in FilterAvailableDocumentation(allDocs, vorgang)) item.AvailableDocumentation.Add(new DocumentationItem(doc, nameById.GetValueOrDefault(doc.StudentId, ""))); foreach (var entry in vorgang.ClassRegisterEntries.OrderByDescending(e => e.Date)) item.ClassRegisterRows.Add(new VorgangClassRegisterEntryRow(entry)); Cases.Add(item); } Status = OnlyOpen ? $"{Cases.Count} offene Vorgänge" : $"{Cases.Count} Vorgänge"; } finally { Busy = false; NotifyState(); } } private void NotifyState() => OnPropertyChanged(nameof(HasCases)); [RelayCommand] private async Task AddVorgang() { if (OnEditVorgang is null) return; var result = await OnEditVorgang(_rosterStudentOptions, null); if (result is null) return; _vorgaenge.Save(result); await LoadInternal(); } [RelayCommand] private async Task EditVorgang(VorgangItem? item) { if (item is null || OnEditVorgang is null) return; var result = await OnEditVorgang(_rosterStudentOptions, item.Model); if (result is null) return; _vorgaenge.Save(result); await LoadInternal(); } [RelayCommand] private async Task ToggleStatus(VorgangItem? item) { if (item is null) return; item.Model.Status = item.Model.Status == VorgangStatus.Open ? VorgangStatus.Closed : VorgangStatus.Open; item.Model.ClosedAt = item.Model.Status == VorgangStatus.Closed ? DateTime.UtcNow : null; _vorgaenge.Save(item.Model); await LoadInternal(); } [RelayCommand] private async Task DeleteVorgang(VorgangItem? item) { if (item is null) return; if (OnConfirmDeleteVorgang is not null && !await OnConfirmDeleteVorgang(item)) return; _vorgaenge.Delete(item.Model.Id); await LoadInternal(); } [RelayCommand] private async Task CreateAndLinkDocumentation(VorgangItem? item) { if (item is null || OnEditDocumentation is null) return; var studentOptions = _rosterStudentOptions.Where(s => item.Model.StudentIds.Contains(s.Id)).ToList(); var defaultStudentId = item.Model.StudentIds.FirstOrDefault(); var result = await OnEditDocumentation(defaultStudentId, studentOptions, null); if (result is null) return; _documentation.Save(result); item.Model.DocumentationIds.Add(result.Id); _vorgaenge.Save(item.Model); await LoadInternal(); } /// Dokumentation der Vorgangs-Schüler*innen, die noch nicht mit diesem Vorgang verknüpft ist — /// reine, ohne Repository-Zugriff testbare Filterlogik (gleiches Muster wie /// ). public static List FilterAvailableDocumentation(IReadOnlyList allDocs, Vorgang vorgang) { var linkedIds = vorgang.DocumentationIds.ToHashSet(); return allDocs.Where(d => vorgang.StudentIds.Contains(d.StudentId) && !linkedIds.Contains(d.Id)).ToList(); } private async Task LinkDocumentation(VorgangItem item, DocumentationItem doc) { item.Model.DocumentationIds.Add(doc.Model.Id); _vorgaenge.Save(item.Model); await LoadInternal(); } private async Task UnlinkDocumentation(VorgangItem item, DocumentationItem doc) { item.Model.DocumentationIds.Remove(doc.Model.Id); _vorgaenge.Save(item.Model); await LoadInternal(); } private async Task RemoveClassRegisterEntry(VorgangItem item, VorgangClassRegisterEntryRow row) { item.Model.ClassRegisterEntries.Remove(row.Model); _vorgaenge.Save(item.Model); await LoadInternal(); } // ── Anheften eines Klassenbuch-Eintrags aus dem Klassenbuch-Tab (ClassTeacherDetailsViewModel, // per Konstruktor-Injection dieser Instanz — kein Func-Hook, da hier keine UI im Spiel ist, // nur Zugriff auf schon geladene Daten dieser Geschwister-ViewModel-Instanz) ──────────────── public IReadOnlyList RosterStudentOptions => _rosterStudentOptions; /// Offene Vorgänge, an die sich ein Klassenbuch-Eintrag für diese/n Schüler*in anheften lässt /// (Namensabgleich wie beim restlichen Klassenlehrer-Bereich, siehe UntisNameMatching). public List OpenCasesForStudent(string untisDisplayName) { var match = _rosterStudentOptions.FirstOrDefault(s => UntisNameMatching.NamesMatch(s.Name, untisDisplayName)); if (match is null) return []; return Cases.Where(c => c.IsOpen && c.Model.StudentIds.Contains(match.Id)).ToList(); } public async Task PinClassRegisterEntryAsync(Guid vorgangId, VorgangClassRegisterEntry entry) { var vorgang = _vorgaenge.GetById(vorgangId); if (vorgang is null) return; vorgang.ClassRegisterEntries.Add(entry); _vorgaenge.Save(vorgang); await LoadInternal(); } public async Task CreateAndPinAsync(string title, Guid studentId, VorgangClassRegisterEntry entry) { var vorgang = new Vorgang { Title = title, StudentIds = [studentId] }; vorgang.ClassRegisterEntries.Add(entry); _vorgaenge.Save(vorgang); await LoadInternal(); return vorgang; } }